const ModbusRTU = require('modbus-serial')
const crc16 = require('modbus-serial/utils/crc16')

/**
 * Parse the data for a Modbus -
 * Preset Single Registers (FC=06)
 *
 * @param {Buffer} data the data buffer to parse.
 * @param {Function} next the function to call next.
 */
function _readFC6 (data, next) {
  var dataAddress = data.readUInt16BE(3)
  var value = data.readUInt16BE(5)

  if (next) {
    next(null, {'address': dataAddress, 'value': value})
  }
}

/**
 * Parse the data for a Modbus -
 * Read Input Registers (FC=04, 03)
 *
 * @param {Buffer} data the data buffer to parse.
 * @param {Function} next the function to call next.
 */
function _readFC4 (data, next) {
  var length = data.readUInt8(3)
  var contents = []

  for (var i = 0; i < length; i += 2) {
    var reg = data.readUInt16BE(i + 4)
    contents.push(reg)
  }

  if (next) {
    next(null, {'data': contents, 'buffer': data.slice(4, 4 + length)})
  }
}
/**
 * Wrapper method for writing to a port with timeout. <code><b>[this]</b></code> has the context of ModbusRTU
 * @param {Buffer} buffer The data to send
 * @private
 */
function _writeBufferToPort (buffer) {
  var transaction = this._transactions[this._transactionId]

  this._port.write(buffer)
  transaction._timeoutHandle = _startTimeout(this._timeout, transaction.next)
}

/**
 * Starts the timeout timer with the given duration.
 * If the timeout ends before it was cancelled, it will call the callback with an error.
 * @param {number} duration the timeout duration in milliseconds.
 * @param {Function} next the function to call next.
 * @return {number} The handle of the timeout
 * @private
 */
function _startTimeout (duration, next) {
  if (!duration) {
    return undefined
  }
  return setTimeout(function () {
    if (next) {
      next('Timed out')
    }
  }, duration)
}

/**
 * Cancel the given timeout
 * @param {number} timeoutHandle The handle of the timeout
 * @private
 */
function _cancelTimeout (timeoutHandle) {
  clearTimeout(timeoutHandle)
}
/**
 * Class extending the ModbusRTU to support Zigbee transport
 * the difference of zibgee transport with serial 854 transport
 * is that the modbus id(1 Byte) is replaced with zigbee source
 * network id(2 bytes)
 */
class ModbusRTUEx extends ModbusRTU {
  constructor (port) {
    super(port)
  }

  /**
   * Open the serial port and register Modbus parsers
   *
   * @param useZigbee {boolean} whether transfer over zigbee or 845
   * @param {Function} callback the function to call next on open success
   *      of failure.
   */
  open (useZigbee, callback) {
    let modbus = this
    // the 485 version set nextAddress property
    if (!useZigbee) {
      super.open(callback)
      return
    }

    // open the serial port
    modbus._port.open(function (error) {
      if (error) {
        /* On serial port open error
         * call next function
         */
        if (callback) {
          callback(error)
        }
      } else {
        /* On serial port open OK
         * call next function
         */
        if (callback) {
          callback(error)
        }

        /* On serial port success
         * register the modbus parser functions
         */
        modbus._port.on('data', function (data) {
          // set locale helpers variables
          var transaction = modbus._transactions[modbus._transactionId]

          /* cancel the timeout */
          _cancelTimeout(transaction._timeoutHandle)
          transaction._timeoutHandle = undefined

          // check local cmd for zigbee module
          if (transaction.header) {
            // data length error
            if (data.length !== transaction.nextLength) {
              error = 'Data length error, expected ' +
              transaction.nextLength + ' got ' + data.length
              if (transaction.next) {
                transaction.next(error)
              }
              return
            }

            if (transaction.header.compare(data, 0, transaction.nextLength - 1)) {
              error = 'response header error'
              if (transaction.next) {
                transaction.next(error)
              }
              return
            }
            if (data.readUInt8(transaction.nextLength - 1) !== 0) {
              error = 'reponse with error code: ' + data.readUInt8(transaction.nextLength - 1)
              if (transaction.next) {
                transaction.next(error)
              }
              return
            }
            // success
            if (transaction.next) {
              transaction.next(null)
            }
            return
          }
          /* check incoming data
           */

          /* check minimal length
           */
          if (data.length < 6) {
            error = 'Data length error, expected ' +
            transaction.nextLength + ' got ' + data.length
            if (transaction.next) {
              transaction.next(error)
            }
            return
          }

          /* check message CRC
           * if CRC is bad raise an error
           */
          const crcIn = data.readUInt16LE(data.length - 2)
          if (crcIn !== crc16(data.slice(0, -2))) {
            error = 'CRC error'
            if (transaction.next) {
              transaction.next(error)
            }
            return
          }

          // if crc is OK, read address and function code
          const dstAddress = data.readUInt16BE(0)
          const code = data.readUInt8(2)

          /* check for modbus exception
           */
          if (data.length === 6 &&
            code === (0x80 | transaction.nextCode)) {
            error = 'Modbus exception ' + data.readUInt8(3)
            if (transaction.next) {
              transaction.next(error)
            }
            return
          }

          /* check message length
           * if we do not expect this data
           * raise an error
           */
          if (data.length !== transaction.nextLength) {
            error = 'Data length error, expected ' +
            transaction.nextLength + ' got ' + data.length
            if (transaction.next) {
              transaction.next(error)
            }
            return
          }

          /* check message address and code
           * if we do not expect this message
           * raise an error
           */
          if (dstAddress !== transaction.dstAddress || code !== transaction.nextCode) {
            error = 'Unexpected data error, expected ' +
              transaction.dstAddress + ' got ' + dstAddress
            if (transaction.next) {
              transaction.next(error)
            }
            return
          }

          /* parse incoming data
           */

          switch (code) {
            case 1:
            case 2:
              // Read Coil Status (FC=01)
              // Read Input Status (FC=02)
              // _readFC2(data, transaction.next)
              transaction.next('unsupported function code')
              break
            case 3:
            case 4:
              // Read Input Registers (FC=04)
              // Read Holding Registers (FC=03)
              _readFC4(data, transaction.next)
              break
            case 5:
              // Force Single Coil
              // _readFC5(data, transaction.next)
              transaction.next('unsupported function code')
              break
            case 6:
              // Preset Single Register
              _readFC6(data, transaction.next)
              break
            case 15:
            case 16:
              // Force Multiple Coils
              // Preset Multiple Registers
              // _readFC16(data, transaction.next)
              transaction.next('unsupported function code')
              break
          }
        })
      }
    })
  }

  /**
   * Wirte a Modbus "Read Holding Registers" (FC=03) to zigbee transport.
   *
   * @param {number} srcAddress the source network address of zigbee.
   * @param {number} dstAddress the destination network address of zigbee.
   * @param {number} dataAddress the Data Address of the first Registers.
   * @param {number} length of the total number of registers requested.
   * @param {Function} next the callback function to call next.
   */
  writeFC3 (srcAddress, dstAddress, dataAddress, length, next) {
    this.writeFC4(srcAddress, dstAddress, dataAddress, length, next, 3)
  }

  /**
   * Write a Modbus "Read Holding Registers" (FC=04) to zigbee transport.
   *
   * @param {number} srcAddress the source network address of zigbee.
   * @param {number} dstAddress the destination network address of zigbee.
   * @param {number} dataAddress the Data Address of the first Registers.
   * @param {number} length of the total number of registers requested.
   * @param {Function} next the callback function to call next
   */
  writeFC4 (srcAddress, dstAddress, dataAddress, length, next, code) {
    if (!dstAddress) {
      super.writeFC4(srcAddress, dataAddress, length, next, code)
      return
    }
    // fucnton code default to 4
    code = code || 4

    // check port is actually open before attempting writeFC3
    if (this._port.isOpen() === false) {
      var error = 'Port Not Open'
      if (next) { next(error) }
      return
    }

    // set state variables
    this._transactions[this._transactionId] = {
      srcAddress: srcAddress,
      dstAddress: dstAddress,
      nextCode: code,
      nextLength: 4 + 2 * length + 2,
      next: next
    }

    const codeLength = 7
    var buf = Buffer.alloc(codeLength + 2)

    buf.writeUInt16BE(srcAddress, 0)
    buf.writeUInt8(code, 2)
    buf.writeUInt16BE(dataAddress, 3)
    buf.writeUInt16BE(length, 5)
    // add crc bytes to Buffer
    buf.writeUInt16LE(crc16(buf.slice(0, -2)), codeLength)

    this._port.setId(dstAddress)
    // write buffer to serial port
    _writeBufferToPort.call(this, buf)
  }

  /**
   * Write a Modbus "Preset Single Register " (FC=6) to zigbee transport.
   *
   * @param {number} srcAddress the source network address of zigbee.
   * @param {number} dstAddress the destination network address of zigbee.
   * @param {number} dataAddress the Data Address of the register.
   * @param {number} value the value to write to the register.
   * @param {Function} next the function to call next.
   */
  writeFC6 (srcAddress, dstAddress, dataAddress, value, next) {
    if (!dstAddress) {
      super.writeFC6(srcAddress, dataAddress, value, next)
      return
    }
    const code = 6

    // check port is actually open before attempting write
    if (this._port.isOpen() === false) {
      var error = 'Port Not Open'
      if (next) next(error)
      return
    }

    // set state variables
    this._transactions[this._transactionId] = {
      srcAddress: srcAddress,
      dstAddress: dstAddress,
      nextCode: code,
      nextLength: 9,
      next: next
    }

    const codeLength = 7
    var buf = Buffer.alloc(codeLength + 2) // add 2 crc bytes

    buf.writeUInt16BE(srcAddress, 0)
    buf.writeUInt8(code, 2)
    buf.writeUInt16BE(dataAddress, 3)
    buf.writeUInt16BE(value, 5)

    // add crc bytes to buffer
    buf.writeUInt16LE(crc16(buf.slice(0, -2)), codeLength)

    this._port.setId(dstAddress)
    // write buffer to serial port
    _writeBufferToPort.call(this, buf)
  }

  /**
   * Modify the target zigbee network address
   *
   * @param {number} dstAddress the destination network address of zigbee.
   * @param {Function} next the function to call next.
   */
  writeDstAddress (dstAddress, next) {
    // check port is actually open before attempting writeFC3
    if (this._port.isOpen() === false) {
      var error = 'Port Not Open'
      if (next) { next(error) }
      return
    }

    // construct cmd for modify target net address
    const header = Buffer.from([0xDE, 0xDF, 0xEF, 0xD2])

    // set state variables
    this._transactions[this._transactionId] = {
      header: header,
      nextLength: 5,
      next: next
    }

    let buf = Buffer.alloc(2)
    buf.writeUInt16BE(dstAddress)
    buf = Buffer.concat([header, buf])

    // write buffer to serial port
    _writeBufferToPort.call(this, buf)
  }

}

// exports
module.exports = ModbusRTUEx
